K Most Frequent Strings
Find the k most frequently occurring strings in an array, and return them sorted by frequency in descending order. If two strings have the same frequency, sort them in lexicographical order.
Example:
Input: strs = ['go', 'coding', 'byte', 'byte', 'go', 'interview', 'go'], k = 2
Output: ['go', 'byte']
Explanation: The strings "go" and "byte" appear the most frequently, with frequencies of 3 and 2, respectively.
Constraints:
k ≤ n, wherendenotes the length of the array.
Intuition - Max-Heap
The two main challenges to this problem are:
- Identifying the
kmost frequent strings. - Sorting those strings first by frequency and then lexicographically.
For now, let's concentrate on identifying the most frequent strings and address lexicographical ordering afterward.
First, we need a way to keep track of the frequencies of each string. We can use a hash map for this, where the keys represent the strings and the values represent frequencies:
The most straightforward approach is to obtain an array containing the strings from the hash map, sorted by frequency in descending order. The k most frequent strings would be the first k strings in this array.
The main inefficiency of this solution is that it involves sorting all n strings, even though we only need the top k frequent ones to be sorted.
Something useful to consider: if we remove the most frequent string, the new most frequent string after this removal represents the second-most frequent overall. By repeatedly identifying and removing the most frequent string k times, we efficiently obtain our answer.
To implement this idea, we need a data structure that allows efficient access to the most frequent string at any time. A max-heap is perfect for this.
Max-heap
Let's find the k most frequent strings from the previous input, this time using a max-heap. First, populate the heap with each string along with their frequencies.
One way to populate the heap is to push all n strings into it one by one, which will take time. Instead, we can perform the heapify operation on an array containing all the string-frequency pairs to create the max-heap in time.
To collect the k most frequent strings, pop off the top element from the heap k times and store the corresponding strings in the output array res:
Now, we just need to ensure that when two strings have the same frequency, the one that comes first lexicographically has a higher priority in the heap. To do this, we can define a custom comparator for the heap that prioritizes strings lexicographically when their frequencies match, as demonstrated in the implementation below.
Implementation - Max-Heap
We create a Pair class for string-frequency pairs, enabling us to customize priority using a custom comparator.
from typing import List
from collections import Counter
import heapq
class Pair:
def __init__(self, str, freq):
self.str = str
self.freq = freq
# Define a custom comparator.
def __lt__(self, other):
# Prioritize lexicographical order for strings with equal frequencies.
if self.freq == other.freq:
return self.str < other.str
# Otherwise, prioritize strings with higher frequencies.
return self.freq > other.freq
def k_most_frequent_strings_max_heap(strs: List[str], k: int) -> List[str]:
# We use 'Counter' to create a hash map that counts the frequency of each string.
freqs = Counter(strs)
# Create the max heap by performing heapify on all string-frequency pairs.
max_heap = [Pair(str, freq) for str, freq in freqs.items()]
heapq.heapify(max_heap)
# Pop the most frequent string off the heap 'k' times and return these 'k' most
# frequent strings.
return [heapq.heappop(max_heap).str for _ in range(k)]class Pair {
constructor(str, freq) {
this.str = str;
this.freq = freq;
}
}
function kMostFrequentStringsMaxHeap(strs, k) {
// We use a Map to count the frequency of each string.
const freqs = new Map();
for (const str of strs) {
freqs.set(str, (freqs.get(str) || 0) + 1);
}
// Create the max heap pairs from all string-frequency pairs.
const pairs = [];
for (const [str, freq] of freqs.entries()) {
pairs.push(new Pair(str, freq));
}
// Prioritize lexicographical order for strings with equal frequencies.
// Otherwise, prioritize strings with higher frequencies.
pairs.sort((a, b) => {
if (a.freq === b.freq) {
return a.str.localeCompare(b.str);
}
return b.freq - a.freq;
});
// Pop the most frequent string off the heap 'k' times and return these 'k' most
// frequent strings.
return pairs.slice(0, k).map(pair => pair.str);
}import java.util.*;
class Pair {
String str;
int freq;
public Pair(String str, int freq) {
this.str = str;
this.freq = freq;
}
}
public class Solution {
public List<String> kMostFrequentStringsMaxHeap(String[] strs, int k) {
// We use HashMap to count the frequency of each string.
Map<String, Integer> freqs = new HashMap<>();
for (String str : strs) {
freqs.put(str, freqs.getOrDefault(str, 0) + 1);
}
// Define a custom comparator:
// Prioritize lexicographical order for strings with equal frequencies.
// Otherwise, prioritize strings with higher frequencies.
PriorityQueue<Pair> maxHeap = new PriorityQueue<>((a, b) -> {
if (a.freq == b.freq) {
return a.str.compareTo(b.str);
}
return Integer.compare(b.freq, a.freq);
});
for (Map.Entry<String, Integer> entry : freqs.entrySet()) {
maxHeap.offer(new Pair(entry.getKey(), entry.getValue()));
}
// Pop the most frequent string off the heap 'k' times and return these 'k' most
// frequent strings.
List<String> res = new ArrayList<>();
for (int i = 0; i < k; i++) {
res.add(maxHeap.poll().str);
}
return res;
}
}Complexity Analysis
Time complexity: The time complexity of k_most_frequent_strings_max_heap is .
- It takes time to count the frequency of each string using
Counter, and to build themax_heap. - We also pop off the top of the heap times, with each
popoperation taking time.
Therefore, the overall time complexity is .
Space complexity: The space complexity is because the hash map and heap store at most pairs. Note that the output array is not considered in the space complexity.
Intuition - Min-Heap
As a follow up, your interviewer may ask you to modify your solution to reduce the space used by the heap.
In the previous approach, we ended up storing up to n items in the heap. However, since we only need the k most frequent characters, is there a way to maintain a heap with a space complexity of ?
An important observation is that when our heap exceeds size k, we can discard the lowest frequency strings until the heap's size is reduced to k again. We can do this because those discarded strings definitely won't be among the k most frequent strings.
However, we can't implement this strategy with a max-heap because we won't have access to the lowest frequency string. Instead, we need to use a min-heap.
Let's observe how this works over an example:
In the end, the strings remaining in the heap are our top k frequent strings:
To retrieve these strings, pop them from the heap until it's empty. Because we're using a min-heap, we're popping off the less frequent strings first. So, we need to reverse the order of the retrieved strings before returning the result:
from typing import List
from collections import Counter
import heapq
class Pair:
def __init__(self, str, freq):
self.str = str
self.freq = freq
# Since this is a min-heap comparator, we can use the same comparator as the one
# used in the max-heap, but reversing the inequality signs to invert the priority.
def __lt__(self, other):
if self.freq == other.freq:
return self.str > other.str
return self.freq < other.freq
def k_most_frequent_strings_min_heap(strs: List[str], k: int) -> List[str]:
freqs = Counter(strs)
min_heap = []
for str, freq in freqs.items():
heapq.heappush(min_heap, Pair(str, freq))
# If heap size exceeds 'k', pop the lowest frequency string to ensure the heap
# only contains the 'k' most frequent words so far.
if len(min_heap) > k:
heapq.heappop(min_heap)
# Return the 'k' most frequent strings by popping the remaining 'k' strings from
# the heap. Since we're using a min-heap, we need to reverse the result after
# popping the elements to ensure the most frequent strings are listed first.
res = [heapq.heappop(min_heap).str for _ in range(k)]
res.reverse()
return resclass Pair {
constructor(str, freq) {
this.str = str;
this.freq = freq;
}
}
function kMostFrequentStringsMinHeap(strs, k) {
const freqs = new Map();
for (const str of strs) {
freqs.set(str, (freqs.get(str) || 0) + 1);
}
// Since this is a min-heap comparator, we can use the same comparator as the one
// used in the max-heap, but reversing the inequality signs to invert the priority.
const compare = (a, b) => {
if (a.freq === b.freq) {
return b.str.localeCompare(a.str);
}
return a.freq - b.freq;
};
const minHeap = [];
for (const [str, freq] of freqs.entries()) {
minHeap.push(new Pair(str, freq));
minHeap.sort(compare);
// If heap size exceeds 'k', pop the lowest frequency string to ensure the heap
// only contains the 'k' most frequent words so far.
if (minHeap.length > k) {
minHeap.shift();
}
}
// Return the 'k' most frequent strings by popping the remaining 'k' strings from
// the heap. Since we're using a min-heap, we need to reverse the result after
// popping the elements to ensure the most frequent strings are listed first.
const res = minHeap.map(pair => pair.str);
res.reverse();
return res;
}import java.util.*;
class Pair {
String str;
int freq;
public Pair(String str, int freq) {
this.str = str;
this.freq = freq;
}
}
public class Solution {
public List<String> kMostFrequentStringsMinHeap(String[] strs, int k) {
Map<String, Integer> freqs = new HashMap<>();
for (String str : strs) {
freqs.put(str, freqs.getOrDefault(str, 0) + 1);
}
// Since this is a min-heap comparator, we can use the same comparator as the one
// used in the max-heap, but reversing the inequality signs to invert the priority.
PriorityQueue<Pair> minHeap = new PriorityQueue<>((a, b) -> {
if (a.freq == b.freq) {
return b.str.compareTo(a.str);
}
return Integer.compare(a.freq, b.freq);
});
for (Map.Entry<String, Integer> entry : freqs.entrySet()) {
minHeap.offer(new Pair(entry.getKey(), entry.getValue()));
// If heap size exceeds 'k', pop the lowest frequency string to ensure the heap
// only contains the 'k' most frequent words so far.
if (minHeap.size() > k) {
minHeap.poll();
}
}
// Return the 'k' most frequent strings by popping the remaining 'k' strings from
// the heap. Since we're using a min-heap, we need to reverse the result after
// popping the elements to ensure the most frequent strings are listed first.
List<String> res = new ArrayList<>();
while (!minHeap.isEmpty()) {
res.add(minHeap.poll().str);
}
Collections.reverse(res);
return res;
}
}Complexity Analysis
Time complexity: The time complexity of k_most_frequent_strings_min_heap is .
- It takes time to count the frequency of each string using
Counter. - To populate the heap, we push words onto it, with each
pushandpopoperation taking time. This takes time. - Then, we extract strings from the heap by performing the
popoperation times. This takes time. - Finally, we reverse the output array, which takes time.
Therefore, the overall time complexity is .
Space complexity: The space complexity is because the hash map stores at most pairs, whereas the heap only takes up space. The res array is not considered in the space complexity.